// 需求是: 读取 view.tsx 里面的 信息 (defaultProps)，追加到 edit.tsx 里面
// 但是在 webpack 配置中这样写:
//
// entry: {
//   view: "./src/view/index.tsx",
//   edit: "./src/edit/index.tsx"
// },
//
// 会有一个问题 - view 和 edit 的被执行顺序不确定，搜索了很多，没有找到什么可行的方案
//
// 现在将处理逻辑进行修改，view 和 edit 直接走 webpack loader，在这个 webpack loader 中
// 如果先处理的是 view，则将 view 处理一遍，标记下 isViewProcessed
// 如果先处理的是 edit，isViewProcessed 为 false 则将 view 处理一遍，并且标记 view 被处理过了

import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { babelPlugin, babelPreset } from "../../cfg/base";
import { Manifest } from "../../../cmd/widget/manifest";
import { getOptions } from "loader-utils";

const babel = require("@babel/core");
const t = require("@babel/types");

const projDir = process.cwd();
const viewPath = path.resolve(projDir, "src", "view", "index.tsx");
const editPath = path.resolve(projDir, "src", "edit", "index.tsx");

let isView = false;
let isEdit = false;
let isViewProcessed = false;

const babelOpts = {
  plugins: [
    babelPlugin("@babel/plugin-proposal-class-properties"),
    babelPlugin("@babel/plugin-proposal-export-default-from"),
    [babelPlugin("babel-plugin-import"), { libraryName: "antd", style: true }]
  ],
  presets: [
    babelPreset("@babel/preset-typescript"),
    babelPreset("@babel/preset-react"),
    babelPreset("@babel/preset-env")
  ]
};

const viewInfo = {
  defaultExportName: "",
  classProperties: new Map<string, any>()
};

const editInfo = {
  defaultExportName: "",
  declaratorMap: new Map<string, any>(),
  defaultExport: {} as any,
  defaultProps: null as any
};

const buildRegisterWidgetNodeDebug = (id: any, isProxy = false) => {
  const wns = "xwidget_dev";
  const ipt = t.importDeclaration(
    [t.importSpecifier(t.identifier("widgetCenter"), t.identifier("widgetCenter"))],
    t.stringLiteral("@qianmi/x-site-core")
  );

  const tpl = babel.parse("widgetCenter.registerWidget()");
  const exprStmt = tpl.program.body[0];
  const callExpr = exprStmt.expression;
  callExpr.arguments = [
    typeof id === "string" ? t.identifier(id) : id,
    t.objectExpression([
      t.objectProperty(t.identifier("widgetNameSpace"), t.stringLiteral(wns)),
      t.objectProperty(t.identifier("isProxy"), t.booleanLiteral(isProxy))
    ])
  ];
  return [ipt, exprStmt];
};

const buildRegisterWidgetNodeProd = (id: any, wns: string, isProxy = false) => {
  const tpl = babel.parse("__XSite_widgetInject()");
  const exprStmt = tpl.program.body[0];
  const callExpr = exprStmt.expression;
  callExpr.arguments = [
    typeof id === "string" ? t.identifier(id) : id,
    t.objectExpression([
      t.objectProperty(t.identifier("widgetNameSpace"), t.stringLiteral(wns)),
      t.objectProperty(t.identifier("isProxy"), t.booleanLiteral(isProxy))
    ])
  ];
  return [exprStmt];
};

function processView(): babel.PluginObj {
  return {
    name: "normalize",
    visitor: {
      ExportDefaultDeclaration(p, stats) {
        if (!t.isIdentifier(p.node.declaration) && !t.isClassDeclaration(p.node.declaration)) {
          throw new Error("default export can only be a identifier or classDeclaration");
        }
        const id = t.isIdentifier(p.node.declaration)
          ? p.node.declaration
          : (p.node.declaration as any).id;
        const name = id.name;
        viewInfo.defaultExportName = name;
      }
    },
    post(p) {
      if (isDevMode) {
        const stmts = buildRegisterWidgetNodeDebug(viewInfo.defaultExportName);
        stmts.reverse().forEach(stmt => p.path.pushContainer("body", stmt));
      } else {
        const { name } = require(path.resolve(process.cwd(), "package.json"));
        const oldNameSpace = Manifest.getInstance().get<string>("old-namespace");
        let stmt = buildRegisterWidgetNodeProd(viewInfo.defaultExportName, name);
        p.path.pushContainer("body", stmt);
        if (oldNameSpace) {
          stmt = buildRegisterWidgetNodeProd(viewInfo.defaultExportName, oldNameSpace);
          p.path.pushContainer("body", stmt);
        }
      }
      isViewProcessed = true;
    }
  };
}

function processEdit(): babel.PluginObj {
  return {
    name: "normalize",
    visitor: {
      ExportDefaultDeclaration(p, stats) {
        if (!t.isIdentifier(p.node.declaration) && !t.isClassDeclaration(p.node.declaration)) {
          throw new Error("default export can only be a identifier or classDeclaration");
        }
        const id = t.isIdentifier(p.node.declaration)
          ? p.node.declaration
          : (p.node.declaration as any).id;
        const name = id.name;
        editInfo.defaultExportName = name;
        editInfo.defaultExport = p;
      },
      VariableDeclarator(p, stats) {
        if (t.isIdentifier(p.node.id) && t.isObjectExpression(p.node.init)) {
          editInfo.declaratorMap.set((p.node.id as any).name, p.node.init);
        }
      },
      ClassDeclaration(p, stats) {
        const body = p.node.body.body;
        const { version } = require(path.resolve(process.cwd(), "package.json"));
        for (let i = 0, len = body.length; i < len; ++i) {
          const node: any = body[i];
          if (t.isClassProperty(node) && node.key.name === "defaultProps") {
            if (!t.isObjectExpression(node.value)) {
              throw new Error("defaultProps must be object expression");
            }
            node.value.properties.push(
              t.objectProperty(t.identifier("version"), t.stringLiteral(version))
            );
            editInfo.defaultProps = node.value;
            break;
          }
        }
        const hasDefaultProps = !!editInfo.defaultProps;
        if (!hasDefaultProps) {
          const expr = t.objectExpression([
            t.objectProperty(t.identifier("version"), t.stringLiteral(version))
          ]);
          const node = t.classProperty(t.stringLiteral("defaultProps"), expr);
          body.push(node);
          editInfo.defaultProps = expr;
        }
        const prog = t.Program([t.expressionStatement(editInfo.defaultProps)]);
        try {
          const code = babel.transformFromAst(prog, p.hub.file.code).code;
          // tslint:disable-next-line:prefer-const
          let obj = null;
          // tslint:disable-next-line:no-eval
          eval("obj = " + code);
          Manifest.getInstance().set("defaultProps", obj);
        } catch (e) {
          throw new Error("unable to extract defaultProps: " + e);
        }
      }
    },
    post(p) {
      const defaultProps = editInfo.defaultProps;

      const fnId = p.scope.generateUidIdentifier("fn");
      const fnDec = t.functionDeclaration(fnId, [], t.blockStatement([]));

      const editId = p.scope.generateUidIdentifier("edit");
      const editDec = t.variableDeclaration("const", [
        t.variableDeclarator(editId, t.newExpression(t.identifier(editInfo.defaultExportName), []))
      ]);

      const editDecProps = t.expressionStatement(
        t.assignmentExpression(
          "=",
          t.memberExpression(
            t.memberExpression(editId, t.identifier("defaultProps")),
            t.identifier("props")
          ),
          defaultProps
        )
      );

      const prototype = t.expressionStatement(
        t.assignmentExpression("=", t.memberExpression(fnId, t.identifier("prototype")), editId)
      );
      p.path.pushContainer("body", editDec);
      p.path.pushContainer("body", editDecProps);
      p.path.pushContainer("body", fnDec);
      p.path.pushContainer("body", prototype);
      p.path.pushContainer("body", t.exportDefaultDeclaration(fnId));

      if (isDevMode) {
        const stmts = buildRegisterWidgetNodeDebug(fnId, true);
        stmts.reverse().forEach(stmt => p.path.pushContainer("body", stmt));
      } else {
        const { name } = require(path.resolve(process.cwd(), "package.json"));
        const oldNameSpace = Manifest.getInstance().get<string>("old-namespace");
        let stmt = buildRegisterWidgetNodeProd(fnId, name, true);
        p.path.pushContainer("body", stmt);
        if (oldNameSpace) {
          stmt = buildRegisterWidgetNodeProd(fnId, oldNameSpace, true);
          p.path.pushContainer("body", stmt);
        }
      }
    }
  };
}

const transformFile = util.promisify(babel.transformFile);
const readFile = util.promisify(fs.readFile);

let isDevMode = false;
const updateMode = (opts: any) => {
  isDevMode = opts && opts.dev === true;
};

export default async function(source: string) {
  const options = getOptions(this);
  updateMode(options);

  isView = this.resource === viewPath;
  isEdit = this.resource === editPath;

  const done = this.async();

  if (isView && !isViewProcessed) {
    const result = await transformFile(this.resource, {
      plugins: [processView, ...babelOpts.plugins],
      presets: babelOpts.presets
    });
    done(null, result.code);
    return;
  }

  if (isEdit) {
    const result = await transformFile(this.resource, {
      plugins: [processEdit, ...babelOpts.plugins],
      presets: babelOpts.presets
    });
    done(null, result.code);
    return;
  }

  done(null, source);
}
